iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
Software Development

Python十翼:與未來的自己對話系列 第 11

[Day11] 四翼 - Descriptor:Descriptor存取設計(1)

  • 分享至 

  • xImage
  •  

descriptor就像是Python的倉庫管理員之一,在某些情況下(如[Day10]所述):

  • 當其為non-data descriptor時,可以提供取的功能。
  • 當其為data descriptor時,同時提供存取功能。

所以要從哪邊取值及要將值存去哪裡,是寫descriptor時,最須注意的地方。

一般來說,有兩個地方可以考慮,一個是desc_instance內,另一個是instance.__dict__,兩者都各有一些眉角。

接下來兩天,我們將練習數個data descriptor的寫法(註1)。今天會先分享一些有潛在問題的寫法,透過了解各方法的缺點或限制,漸漸學習反思。明天再分享一些通用的寫法。

以下將會用desc_instance來代稱data descriptor instance

方法1

方法1試著將給定的值作為instance variable存在desc_instance內,即# 01中的self._value

# 01
class Desc:
    def __get__(self, instance, owner_cls):
        return self._value

    def __set__(self, instance, value):
        self._value = value


class MyClass:
    x = Desc()


if __name__ == '__main__':
    my_inst1, my_inst2 = MyClass(), MyClass()

    # my_inst1
    my_inst1.x = 1
    print(f'{my_inst1.x=}')  # 1

    # my_inst2
    print(f'{my_inst2.x=}')  # 1
    my_inst2.x = 2
    print(f'{my_inst2.x=}')  # 2

    # my_inst1.x also changed
    print(f'{my_inst1.x=}')  # 2...not 1
my_inst1.x=1
my_inst2.x=1
my_inst2.x=2
my_inst1.x=2
  • MyClass中建立名為xdesc_instance
  • 生成my_inst1my_inst2兩個instance。此時若使用my_inst1.xmy_inst2.x,將會呼叫x.__get__取值:若使用my_inst1.x = 1my_inst2.x = 2,將會呼叫x.__set__賦值。
  • 透過my_inst1.x = 11指定給x內的self._value,並確認my_inst1.x的確為1
  • 接著觀察my_inst2.x,發現其已經有1這個值。這是因為x現在是一個class variable,所以my_inst2.x就是my_inst1.x
  • my_inst2.x = 2會將2指定給x內的self._value,也就是說my_inst1.xmy_inst2.x的回傳值都會是2

使用限制與缺點

  • 使用前需先透過類似my_inst1.x = 1的語法,指定值給self._value
  • 所有MyClass生成的instance都擁有能修改x的權力。

方法2

方法2試著將給定的值作為instance variable存在instance本身,即# 02中的instance.hardcoded_name

# 02
class Desc:
    def __get__(self, instance, owner_cls):
        return getattr(instance, 'hardcoded_name', None)

    def __set__(self, instance, value):
        setattr(instance, 'hardcoded_name', value)


class MyClass:
    x = Desc()
    y = Desc()


if __name__ == '__main__':
    my_inst = MyClass()
    print(f'{my_inst.__dict__=}')  # {}

    my_inst.x = 1
    print(f'{my_inst.x=}')  # 1
    print(f'{my_inst.__dict__=}')  # {'hardcoded_name': 1}

    my_inst.x = 2
    print(f'{my_inst.x=}')  # 2
    print(f'{my_inst.y=}')  # 2
    print(f'{my_inst.__dict__=}')  # {'hardcoded_name': 2}
my_inst.__dict__={}
my_inst.x=1
my_inst.__dict__={'hardcoded_name': 1}
my_inst.x=2
my_inst.y=2
my_inst.__dict__={'hardcoded_name': 2}
  • MyClass中建立xy兩個desc_instance
  • 首先觀察my_inst.__dict__為一空dict
  • 透過my_inst.x = 11指定給my_inst.hardcoded_name,透過再次觀察my_inst.__dict__,可以確認hardcoded_name已在instance.__dict__中,且其值為1
  • 由於我們的my_inst只有準備一個hardcoded_name來作為存取descriptor的倉庫。所以當我們使用my_inst.x = 2時,其實相當於將2指定給my_inst.hardcoded_name,透過再次觀察MyClass.__dict__,可以確認hardcoded_name已在instance.__dict__中,且其值已變為2

使用限制與缺點

  • MyClass中,所有Desc生成的desc_instance將會共享一個固定的instance variable,即instance.hardcoded_name

方法3

方法3嘗試於descriptor內建立一個dict,即# 03self._data。我們以各instance本身為self._datakey,於__set__給定的valueself._datavalue

# 03
class Desc:
    def __init__(self):
        self._data = {}

    def __get__(self, instance, owner_cls):
        return self._data.get(instance)

    def __set__(self, instance, value):
        self._data[instance] = value


class MyClass:
    x = Desc()
    y = Desc()

如果進行和方法1方法2類似的檢查,可以發現方法3也沒有類似的問題,但卻有一個非常大的缺點。

使用限制與缺點

  • 由於self._data是將instance本身作為key,這代表即使我們手動使用del指令刪除了instance,也會是個假象,instance不會被gc(garbage collect),因為至少還有一個strong reference存在。這是個嚴重的memoey leak,該被gcobj卻還是存在,且有機會被存取。
  • 即使不在意memoey leak,我們還必須確定instancehashalbe,才能作為self._datakey

方法4

方法4類似於方法3,但這次我們使用id(instance)self._datakey

# 04
class Desc:
    def __init__(self):
        self._data = {}

    def __get__(self, instance, owner_cls):
        return self._data.get(id(instance))

    def __set__(self, instance, value):
        self._data[id(instance)] = value

        
class MyClass:
    x = Desc()
    y = Desc()

如果進行和方法1方法2類似的檢查,可以發現方法4也沒有類似的問題,且如果我們利用del來刪除instance時,該instance也真的會被gc。僅管如此,方法4還是有一些缺點。

使用限制與缺點

  • 即使我們刪除了instance,其記憶體位置id(instance),仍然以int型態存在self._data中。乍聽好像沒什麼關係,但是Python的記憶體位置是會重複使用的。如果這個記憶體位置被其它obj使用了,我們的descriptor就隱含了這個不相關obj的資訊(雖然機率極低)。
  • 其次即使記憶體位置沒有被重複使用,但如果我們大量使用這類型的descriptor,也會造成很多無謂的記憶體浪費。

充電區

上面幾個方法讓我們對descriptor有了基本的認知。現在我們來充充電,講幾個實作descriptor時常會用到的觀念,為明天descriptor的通用寫法做好準備。

如何取得desc_instance

當我們應用descriptor時,有時需要取得desc_instance,一個常見的做法如下:

def __get__(self, instance, owner_cls):
    if instance is None:
        return self
    ...

__get__一開始,先判斷instance是不是None。如果是None,代表我們是由class來取,直接返回desc_instance。也就是說,當我們想取得desc_instance時,可以利用MyClass.desc來取得。雖然我們大多數情況都是使用instance呼叫desc_instance,但當想觀察desc_instance內部狀態時,可以透過這個小技巧來達成。從明天的方法5開始,我們會於__get__中加上這一段程式碼。

__set_name__

__set_name__signature如下:

__set_name__(self, owner_cls, name)

class在定義時,會自動尋找有實作__set_name__attribute,透過這個方法將他們在class的名字傳入desc_instance註2)。舉例來說,如果Desc實作有__set_name__,那麼MyClass中的'x'(str型態)於MyClass定義時,就會自動透過x.__set_name__傳入desc_instance

class MyClass:
    x = Desc()

這個功能非常方便,可以讓我們在設計descriptor時,取得於MyClass中定義的名字。

__slots__

class實作有__slots__(一般定義為tuple),未被列在其中的attribute,將無法由instance存取,這包括我們一直習以為常使用的__dict__。當然您也可以選擇使用__slots__然後手動將__dict__加進__slots__

# 101可以看出,當MyClass__slots__設定為空的tuple時,我們無法使用my_inst.__dict__。但是在MyClass2中,我們手動加入__dict__後,就可以存取my_inst2.__dict__了。

所以當我們說一個instance.__dict__可用時,代表其class未使用slots又或者有使用slots但是有將__dict__加進__slots__

# 101
class MyClass:
    __slots__ = ()


class MyClass2:
    __slots__ = ('__dict__',)


if __name__ == '__main__':
    my_inst = MyClass()
    print(f'{my_inst.__dict__=}')  # AttributeError

    my_inst2 = MyClass2()
    print(f'{my_inst2.__dict__=}')  # {}

或許您會想,應該不會有很多Class使用slots吧?但事實上,當程式需要大量由同個class生成的instance時,選擇使用slots,可以節省滿多的記憶體消耗。在一些與database相關的ORM應用或iterator class的實作上不算少見。

weak reference

Python內建有weakref module,可以讓我們建立對某objweak reference。當該objstrong reference0時,此weak reference會收到通知,並且在有提供callback function時,呼叫這個function。而weakref.WeakKeyDictionary是一個可以自動幫我們建立及移除weak reference的方便容器。

想要能夠建立weak reference,其必須有__weakref__ attribute__weakref__是一個data descriptor,當我們對一個obj建立weak reference時,這個weak reference object其實就是存在__weakref__中。

當使用slots時,必須手動將__weakref__加進__slots__,否則將無法建立weak reference

class MyClass:
    __slots__ = ('__weakref__',)

備註

註1:至於想實作non-data descriptor的朋友,相信可以由比較複雜的data descriptor中融會貫通而來。

註2:可以參考Data Model對這部份的說明。

Code

本日程式碼傳送門


上一篇
[Day10] 四翼 - Descriptor:Non-Data Descriptor vs Data Descriptor
下一篇
[Day12] 四翼 - Descriptor:Descriptor存取設計(2)
系列文
Python十翼:與未來的自己對話30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言